/*
    This source file is part of GeNa project.
    Copyright © GeNa project 2008
    For the latest info, see http://code.google.com/p/genaproject/ or  http://lumpyproject.forums-rpg.com

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Foobar is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with Foobar.  If not, see <http://www.gnu.org/licenses/>. */

#include <GL/glew.h>
#include <GL/gl3.h>

#ifdef WINDOWS
#define _BASETSD_H_ //Avoid jpeglib INT32 redefinition
#endif

#include <cstdio>
#include <jpeglib.h>
#include <jerror.h>

#include <png.h>
#include <tga.h>

#include <iostream>


#include "image.h"

bool Image::load( std::string filename)
{
    if( !filename.length())
        return false;

    #ifdef WINDOWS
    if( filename[0] == '/')
        filename = filename.substr(1, filename.length()-1);
    #endif

    std::string ext;
    bool find = false;

    for( unsigned int i = filename.length()-1; i > 0; i--)
    {
        if( filename[i] != '.')
        {
            char c;
            if( filename[i] > 'A' && filename[i] < 'Z')
                c= filename[i] - 'A' + 'a';
            else
                c = filename[i];
            ext = std::string(1, c) + ext;
        }
        else
        {
            find = true;
            break;
        }
    }

    if( !find)
        return false;

    if( (ext == "jpeg") || (ext == "jpg"))
        return loadJPEG(filename);
    else if( ext == "png")
        return loadPNG(filename);
    else if( ext == "tga")
        return loadTGA(filename);
    else if( ext == "dds")
        return loadDDS(filename);
    else
        return false;
}

struct jpegErrorMgr
{
  jpeg_error_mgr pub;
  jmp_buf setjmpBuffer;
};

METHODDEF(void) jpegErrorCallBack(j_common_ptr cinfo)
{
  jpegErrorMgr* err = (jpegErrorMgr*)cinfo->err;
  (*err->pub.output_message) (cinfo);
  longjmp(err->setjmpBuffer, 1);
}

bool Image::loadJPEG( std::string filename)
{
    //Initialize JPEG decompression
    FILE * infile;

	if ((infile = fopen(filename.c_str(), "rb")) == NULL)
	{
	    std::cerr << "Can't open " << filename << std::endl;
	    return false;
	}

    jpeg_decompress_struct cinfo;
    jpegErrorMgr jerr;

    cinfo.err = jpeg_std_error(&jerr.pub);
    jerr.pub.error_exit = jpegErrorCallBack;

    if (setjmp(jerr.setjmpBuffer))
    {
        jpeg_destroy_decompress(&cinfo);
        fclose(infile);
        return false;
    }

    jpeg_create_decompress(&cinfo);

	jpeg_stdio_src(&cinfo, infile);

	jpeg_read_header(&cinfo, TRUE);
    jpeg_start_decompress(&cinfo);

    //Fill Image property
    type = GL_TEXTURE_2D;
    imgNum = 1;

    images = new SubImage;
    images->width = cinfo.output_width;
    images->height = cinfo.output_height;
    images->depth = 1;
    switch( cinfo.output_components)
    {
        case 1:
            images->format = GL_RED;
            break;
        case 2:
            images->format = GL_RG;
            break;
        case 3:
            images->format = GL_RGB;
            break;
        default:
            images->format = GL_RGBA;
    }

    images->type = GL_UNSIGNED_BYTE;
    images->face = GL_NONE;
    images->mip = 0;

    //Read to the buffer

    unsigned char* buffer = new unsigned char[cinfo.output_width*cinfo.output_height*cinfo.output_components];
    images->buffer = buffer;

    JSAMPROW* rows = new JSAMPROW[cinfo.output_height];

    for( unsigned int i=0; i < cinfo.output_height; i++)
        rows[i] = buffer + i*cinfo.output_width*cinfo.output_components;

    while(cinfo.output_scanline < cinfo.output_height)
        jpeg_read_scanlines( &cinfo, rows + cinfo.output_scanline, cinfo.output_height - cinfo.output_scanline);

    delete[] rows;

    //Finish the JPEG decompression

    jpeg_finish_decompress(&cinfo);
    jpeg_destroy_decompress(&cinfo);

    fclose(infile);
    return true;
}

#define PNG_BYTES_TO_CHECK 4

bool Image::loadPNG( std::string filename)
{
    //Read file
    FILE *fp = fopen(filename.c_str(), "rb");
    if (!fp)
        return false;

    //Check png header
    png_byte header[PNG_BYTES_TO_CHECK];
    fread(header, 1, PNG_BYTES_TO_CHECK, fp);

    if(png_sig_cmp(header, 0, PNG_BYTES_TO_CHECK))
    {
        fclose(fp);
        return false;
    }

    //Create png structures
    png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
    if (!png_ptr)
    {
        fclose(fp);
        return false;
    }

    png_infop info_ptr = png_create_info_struct(png_ptr);
    if (!info_ptr)
    {
        png_destroy_read_struct(&png_ptr, (png_infopp)NULL, (png_infopp)NULL);
        fclose(fp);
        return false;
    }

    //Catch png errors
    if (setjmp(png_jmpbuf(png_ptr)))
    {
        png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
        fclose(fp);
        return false;
    }

    //Init png reading
    png_init_io(png_ptr, fp);
    png_set_sig_bytes(png_ptr, PNG_BYTES_TO_CHECK);

    //Read png information
    int bit_depth;
    int color_type;
    int interlace_type;


    imgNum = 1; type = GL_TEXTURE_2D;
    images = new SubImage;

    png_read_info(png_ptr, info_ptr);
    png_get_IHDR(png_ptr, info_ptr, (png_uint_32*)&images->width, (png_uint_32*)&images->height,
                &bit_depth, &color_type, &interlace_type,
                NULL, NULL);
    images->depth = 1;

    //Setup transformations
    png_set_packing(png_ptr);

    if (color_type == PNG_COLOR_TYPE_PALETTE)
        png_set_palette_to_rgb(png_ptr);

    if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS))
        png_set_tRNS_to_alpha(png_ptr);

    if(color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_GRAY_ALPHA)
        png_set_gray_to_rgb(png_ptr);

    png_read_update_info(png_ptr, info_ptr);

    images->face = GL_NONE;
    images->mip = 0;

    if( bit_depth == 8)
        images->type = GL_UNSIGNED_BYTE;
    else
        images->type = GL_UNSIGNED_SHORT;

    if( color_type == PNG_COLOR_TYPE_RGB)
        images->format = GL_RGB;
    else
        images->format = GL_RGBA;

    //Allocate buffer and read
    unsigned int rowSize = bit_depth/8 * (color_type == GL_RGB ? 3 : 4) * images->width;
    unsigned char* buffer = new unsigned char[rowSize * images->height];
    images->buffer = buffer;

    unsigned char** row_pointers = new unsigned char*[images->height];

    for( unsigned int i=0; i<images->height; i++)
        row_pointers[i] = buffer + i*rowSize;

    png_read_image(png_ptr, row_pointers);

    delete[] row_pointers;

    //Destroy png structures
    png_read_end(png_ptr, NULL);
    png_destroy_read_struct(&png_ptr, &info_ptr, (png_infopp)NULL);

    fclose(fp);
    return true;
}

void copy( unsigned char* to, unsigned int outSz, unsigned char* from, unsigned int inSz)
{
    unsigned int tmp;

    switch( inSz)
    {
        case 8:
            tmp = *from;
            break;
        case 16:
            tmp = *(unsigned short*)from;
            break;
        case 24:
            #ifdef BIG_ENDIAN
            tmp = ((unsigned int)(from[0]) << 16) + ((unsigned int)(from[1]) << 8) + (unsigned int)(from[2]);
            #else
            tmp = ((unsigned int)(from[2]) << 16) + ((unsigned int)(from[1]) << 8) + (unsigned int)(from[0]);
            #endif
            break;
        break;
        case 32:
            tmp = *(unsigned int*)from;
            break;
    }

    switch( outSz)
    {
        case 8:
            *from = tmp;
            break;
        case 16:
            *(unsigned short*)from = tmp;
            break;
        break;
        case 32:
            *(unsigned int*)from  = tmp;
            break;
    }
}

bool Image::loadTGA( std::string filename)
{
    //Read TGA file

    TGA* tga;

    tga = TGAOpen(const_cast<char*>(filename.c_str()), const_cast<char*>("r"));
    if( !tga || tga->last != TGA_OK)
        return false;

    TGAData data;
    data.flags = TGA_IMAGE_DATA | TGA_IMAGE_ID | TGA_RGB;
    TGAReadImage( tga, &data);

    if( tga->last != TGA_OK)
    {
        TGAClose(tga);
        return false;
    }

    //Set image information
    TGAHeader header = tga->hdr;
    type = GL_TEXTURE_2D;
    imgNum = 1;
    images = new SubImage;

    images->width = header.width;
    images->height = header.height;
    images->depth = 1;

    unsigned int componentSize = header.depth;

    if( componentSize < header.alpha)
        componentSize = header.alpha;

    if( componentSize == 24)
        componentSize = 32;

    switch( componentSize)
    {
        case 8:
            images->type = GL_UNSIGNED_BYTE;
            break;
        case 16:
            images->type = GL_UNSIGNED_SHORT;
            break;
        case 32:
            images->type = GL_UNSIGNED_INT;
            break;
        default:
            TGAClose(tga);
            return false;
    }

    if( header.alpha)
        images->format = GL_RGBA;
    else
        images->format = GL_RGB;

    unsigned int pixelSize = (3 + (bool)header.alpha) * componentSize/8;
    unsigned int imgPixelSize = 3*header.depth + header.alpha;

    //Read data
    unsigned char* buffer = new unsigned char[images->width*images->height*pixelSize];
    images->buffer = buffer;

    for( unsigned int i=0; i< images->width*images->height; i++)
    {
        copy(buffer + i*pixelSize, componentSize, data.img_data + i*imgPixelSize, header.depth);
        copy(buffer + i*pixelSize + componentSize/8, componentSize, data.img_data + i*imgPixelSize + header.depth/8, header.depth);
        copy(buffer + i*pixelSize + 2*componentSize/8, componentSize, data.img_data + i*imgPixelSize + header.depth/4, header.depth);
        copy(buffer + i*pixelSize + 3*componentSize/8, componentSize, data.img_data + i*imgPixelSize + 3*header.depth/8, header.alpha);
    }

    TGAClose(tga);
    return true;
}

bool Image::texImage( SubImage* image, GLenum target, int mipmapLevel, GLenum internalFormat)
{
    switch( target)
    {
        case GL_TEXTURE_1D:
            if(type != GL_TEXTURE_1D)
                return false;
            if( mipmapLevel == -1)
                mipmapLevel = image->mip;

            if( image->swap)
                glPixelStorei( GL_UNPACK_ROW_LENGTH, GL_TRUE );
            glTexImage1D( target, mipmapLevel, internalFormat, image->width, 0, image->format, image->type, (void*)image->buffer);
            if( image->swap)
                glPixelStorei( GL_UNPACK_ROW_LENGTH, GL_FALSE );
            return true;

        case GL_TEXTURE_2D:
        case GL_TEXTURE_RECTANGLE:
        case GL_TEXTURE_DEPTH:
        case GL_TEXTURE_CUBE_MAP_POSITIVE_X:
        case GL_TEXTURE_CUBE_MAP_NEGATIVE_X:
        case GL_TEXTURE_CUBE_MAP_POSITIVE_Y:
        case GL_TEXTURE_CUBE_MAP_NEGATIVE_Y:
        case GL_TEXTURE_CUBE_MAP_POSITIVE_Z:
        case GL_TEXTURE_CUBE_MAP_NEGATIVE_Z:

            if(type != GL_TEXTURE_2D)
                return false;

            if( type == GL_TEXTURE_DEPTH)
                type = GL_TEXTURE_2D;

            if( mipmapLevel == -1)
                mipmapLevel = image->mip;

            if( image->swap)
                glPixelStorei( GL_UNPACK_ROW_LENGTH, GL_TRUE );

            if( (image->format == GL_COMPRESSED_RGB_S3TC_DXT1_EXT)
                || (image->format == GL_COMPRESSED_RGBA_S3TC_DXT3_EXT)
                || (image->format == GL_COMPRESSED_RGBA_S3TC_DXT5_EXT))
                glCompressedTexImage2D( target, mipmapLevel, image->format, image->width, image->height, 0, image->imageSize, (void*)image->buffer);
            else
                glTexImage2D( target, mipmapLevel, internalFormat, image->width, image->height, 0, image->format, image->type, (void*)image->buffer);

            if( image->swap)
                glPixelStorei( GL_UNPACK_ROW_LENGTH, GL_FALSE );
            return true;

        case GL_TEXTURE_3D:
            if(type != GL_TEXTURE_3D)
                return false;

            if( mipmapLevel == -1)
                mipmapLevel = image->mip;

            if( image->swap)
                glPixelStorei( GL_UNPACK_ROW_LENGTH, GL_TRUE );

            glTexImage3D( target, mipmapLevel, internalFormat, image->width, image->height, image->depth, 0, image->format, image->type, (void*)image->buffer);

            if( image->swap)
                glPixelStorei( GL_UNPACK_ROW_LENGTH, GL_FALSE );
            return true;

        case GL_TEXTURE_CUBE_MAP:
            if(type != GL_TEXTURE_CUBE_MAP)
                return false;

            if(type != GL_TEXTURE_2D)
                return false;

            if( mipmapLevel == -1)
                mipmapLevel = image->mip;

            if( image->swap)
                glPixelStorei( GL_UNPACK_ROW_LENGTH, GL_TRUE );

            if( (image->format == GL_COMPRESSED_RGB_S3TC_DXT1_EXT)
                || (image->format == GL_COMPRESSED_RGBA_S3TC_DXT3_EXT)
                || (image->format == GL_COMPRESSED_RGBA_S3TC_DXT5_EXT))
                glCompressedTexImage2D( image->face, mipmapLevel, image->format, image->width, image->height, 0, image->imageSize, (void*)image->buffer);
            else
                glTexImage2D( image->face, mipmapLevel, internalFormat, image->width, image->height, 0, image->format, image->type, (void*)image->buffer);

            if( image->swap)
                glPixelStorei( GL_UNPACK_ROW_LENGTH, GL_FALSE );
            return true;

        default:
            return false;
    }

    //TODO : Check GL errors
}

bool Image::texSubImage( SubImage* image, GLenum target, int mipmapLevel, unsigned int xOffset, unsigned yOffset, unsigned int zOffset)
{
    switch( target)
    {
        case GL_TEXTURE_1D:
            if(type != GL_TEXTURE_1D)
                return false;
            if( mipmapLevel == -1)
                mipmapLevel = image->mip;

            if( image->swap)
                glPixelStorei( GL_UNPACK_ROW_LENGTH, GL_TRUE );
            glTexSubImage1D( target, mipmapLevel, xOffset, image->width, image->format, image->type, (void*)image->buffer);
            if( image->swap)
                glPixelStorei( GL_UNPACK_ROW_LENGTH, GL_FALSE );
            return true;

        case GL_TEXTURE_2D:
        case GL_TEXTURE_RECTANGLE:
        case GL_TEXTURE_DEPTH:
        case GL_TEXTURE_CUBE_MAP_POSITIVE_X:
        case GL_TEXTURE_CUBE_MAP_NEGATIVE_X:
        case GL_TEXTURE_CUBE_MAP_POSITIVE_Y:
        case GL_TEXTURE_CUBE_MAP_NEGATIVE_Y:
        case GL_TEXTURE_CUBE_MAP_POSITIVE_Z:
        case GL_TEXTURE_CUBE_MAP_NEGATIVE_Z:
            if(type != GL_TEXTURE_2D)
                return false;

            if( type == GL_TEXTURE_DEPTH)
                type = GL_TEXTURE_2D;

            if( mipmapLevel == -1)
                mipmapLevel = image->mip;

            if( image->swap)
                glPixelStorei( GL_UNPACK_ROW_LENGTH, GL_TRUE );

            if( (image->format == GL_COMPRESSED_RGB_S3TC_DXT1_EXT)
                || (image->format == GL_COMPRESSED_RGBA_S3TC_DXT3_EXT)
                || (image->format == GL_COMPRESSED_RGBA_S3TC_DXT5_EXT))
                glCompressedTexSubImage2D( target, mipmapLevel, xOffset, yOffset, image->format, image->width, image->height, image->imageSize, (void*)image->buffer);
            else
                glTexSubImage2D( target, mipmapLevel, xOffset, yOffset, image->width, image->height, image->format, image->type, (void*)image->buffer);

            if( image->swap)
                glPixelStorei( GL_UNPACK_ROW_LENGTH, GL_FALSE );
            return true;

        case GL_TEXTURE_3D:
            if(type != GL_TEXTURE_3D)
                return false;

            if( mipmapLevel == -1)
                mipmapLevel = image->mip;

            if( image->swap)
                glPixelStorei( GL_UNPACK_ROW_LENGTH, GL_TRUE );

            glTexSubImage3D( target, mipmapLevel, xOffset, yOffset, zOffset, image->width, image->height, image->depth, image->format, image->type, (void*)image->buffer);

            if( image->swap)
                glPixelStorei( GL_UNPACK_ROW_LENGTH, GL_FALSE );
            return true;

        case GL_TEXTURE_CUBE_MAP:
            if(type != GL_TEXTURE_CUBE_MAP)
                return false;

            if(type != GL_TEXTURE_2D)
                return false;

            if( mipmapLevel == -1)
                mipmapLevel = image->mip;

            if( image->swap)
                glPixelStorei( GL_UNPACK_ROW_LENGTH, GL_TRUE );

            if( (image->format == GL_COMPRESSED_RGB_S3TC_DXT1_EXT)
                || (image->format == GL_COMPRESSED_RGBA_S3TC_DXT3_EXT)
                || (image->format == GL_COMPRESSED_RGBA_S3TC_DXT5_EXT))
                glCompressedTexSubImage2D( image->face, mipmapLevel, xOffset, yOffset, image->format, image->width, image->height, image->imageSize, (void*)image->buffer);
            else
                glTexSubImage2D( image->face, mipmapLevel, xOffset, yOffset, image->width, image->height, image->format, image->type, (void*)image->buffer);

            if( image->swap)
                glPixelStorei( GL_UNPACK_ROW_LENGTH, GL_FALSE );
            return true;

        default:
            return false;
    }

    //TODO : Check GL errors
}

bool Image::texImage( GLenum target, GLenum internalFormat)
{
    for( unsigned int i=0; i < imgNum; i++)
    {
        if( !texImage(&images[i], target, -1, internalFormat))
            return false;
    }

    return true;
}
